﻿using System;
using System.Collections.Generic;
using System.Net;
using System.IO;
using System.Xml;
using System.Web;

#region MIT License text
/*
Copyright (c) 2009 Mark van Straten - Q42 B.V.

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/
#endregion

namespace Q42.Wheels.Api.Nabaztag
{
    /// <summary>
    /// Class based wrapper around the Violet REST service for the nabaztag/tag wifi bunny
    /// </summary>
    public class NabaztagApi
    {
        /// <summary>
        /// Version of the currently connected nabaztag
        /// </summary>
        public enum NabaztagVersion
        {
            /// <summary>
            /// Old version bunny, can not do all fancy stuff the new v2 version can
            /// </summary>
            Nabaztag = 1,
            /// <summary>
            /// New version with RFID reader and more :)
            /// </summary>
            NabaztagTag = 2,
        }

        /// <summary>
        /// URL of the API of Violet
        /// </summary>
        protected readonly String theBurrowUrl = "http://api.nabaztag.com/vl/FR/api.jsp";

        /// <summary>
        /// stored value of Serial
        /// </summary>
        private String serial;
        /// <summary>
        /// Serial of the nabaztag/tag (MAC address from bottom side, without the : separator
        /// </summary>
        public String Serial
        {
            get { return this.serial; }
            set
            {
                if (value.Length != 12)
                    throw new ArgumentOutOfRangeException("Serial should be the MAC-address of your bunny without the dashes (12 chars)");
                this.serial = value;
            }
        }

        /// <summary>
        /// stored value of Token
        /// </summary>
        private String token;
        /// <summary>
        /// Token retrieved from my.violet.net for accessing your bunny
        /// </summary>
        public String Token
        {
            get { return this.token; }
            set
            {
                if (value.Equals(string.Empty))
                    throw new ArgumentOutOfRangeException("No empty token allowed");
                this.token = value;
            }
        }

        /// <summary>
        /// Status if the nabaztag is sleeping
        /// </summary>
        public Boolean IsSleeping
        {
            get{ return GetIsSleeping(); }
            set{ SetRabbitStatus(value); }
        }

        /// <summary>
        /// Which version is our current coupled nabaztag
        /// </summary>
        public NabaztagVersion Version
        {
            get { return GetVersion(); }
        }

        /// <summary>
        /// Get the name of the rabbit
        /// </summary>
        public String Name
        {
            get { return GetName(); }
        }

        /// <summary>
        /// Initialize the API
        /// </summary>
        /// <param name="oSerial">Your nabaztag/tag serial (MAC address from bottom side, without the : separator)</param>
        /// <param name="oToken">Token retrieved from my.violet.net</param>
        public NabaztagApi(string oSerial, string oToken)
        {
            Serial = oSerial;
            Token = oToken;
        }

        /// <summary>
        /// Send an action to the nabaztag service and return the response xml
        /// </summary>
        /// <param name="parameters"></param>
        /// <returns></returns>
        protected XmlDocument SendAction(Dictionary<String, String> parameters)
        {
            if (!(parameters.ContainsKey("sn") && parameters.ContainsKey("token")))
                throw new ArgumentOutOfRangeException("sn and token are required parameters");

            //all actions paramters are now in the parameter list, build querystring of it (htmlencode values)
            String RequestString = "?";
            foreach (KeyValuePair<String, String> kvp in parameters)
                RequestString += String.Format("{0}={1}&", kvp.Key, HttpUtility.HtmlEncode(kvp.Value));

            //send action to API
            HttpWebResponse response = null;
            XmlDocument responseXml = new XmlDocument();
            try
            {
                // Create the web request  
                HttpWebRequest request = WebRequest.Create(theBurrowUrl + RequestString) as HttpWebRequest;

                // Get response  
                response = request.GetResponse() as HttpWebResponse;
                if (request.HaveResponse == true && response != null)
                {
                    // Get the response stream & read response to XML
                    StreamReader reader = new StreamReader(response.GetResponseStream());
                    responseXml.LoadXml(reader.ReadToEnd());
                }
            }
            catch (WebException ex)
            {
                if (ex.Response != null)
                    using (HttpWebResponse errorResponse = (HttpWebResponse)ex.Response)
                    {
                        throw new NabaztagResponseException(String.Format("Response from burrow: '{0}' with the status code '{1}' while accessing url {2}", errorResponse.StatusDescription, errorResponse.StatusCode, theBurrowUrl + RequestString), ex);
                    }
                else
                    throw new NabaztagResponseException(String.Format("Response from burrow: '{0}' while accessing url {2}", ex.Message, theBurrowUrl + RequestString), ex);
            }
            finally
            {
                if (response != null)
                    response.Close();
            }

            //check for faulty response messages from the service
            raisePossibleResultException(responseXml);

            return responseXml;
        }

        /// <summary>
        /// Send only one action at the same time to the nabaztag (saved 5 days in the service)
        /// </summary>
        /// <param name="action"></param>
        /// <returns></returns>
        public string SendAction(NabaztagAction action)
        {
            List<NabaztagAction> actionlist = new List<NabaztagAction>();
            actionlist.Add(action);
            return this.SendActionList(actionlist);
        }

        /// <summary>
        /// Sent list of actions to Nabaztag to display which is saved 5 days in the service
        /// </summary>
        /// <param name="Actions"></param>
        /// <returns></returns>
        /// <remarks>These actions are played together. If you want to play multiple actions after each other you have to sent multiple SentActionLists()</remarks>
        public string SendActionList(List<NabaztagAction> Actions)
        {
            return SendActionList(Actions, 60 * 60 * 24 * 5);//save messages for 5 days
        }

        /// <summary>
        /// Sent list of actions to Nabaztag to display and return the result
        /// </summary>
        /// <param name="Actions"></param>
        /// <param name="SecondsSavedInService">How long in seconds this messages is saved for retrieval trough my.violet.net</param>
        /// <exception cref="ArgumentOutOfRangeException">When multiple times same NabaztagAction is sent or two NabaztagAction with same arguments</exception>
        /// <returns></returns>
        /// <remarks>These actions are played together. If you want to play multiple actions after each other you have to sent multiple SentActionLists()</remarks>
        public string SendActionList(List<NabaztagAction> Actions, Int64 SecondsSavedInService)
        {
            Dictionary<String, String> parameters = GetParameterDictionary();
            try
            {
                parameters.Add("ttlive", SecondsSavedInService.ToString());

                //actions add parameters to the dictionary
                foreach (NabaztagAction action in Actions)
                    action.ToStringList(ref parameters);
            }
            catch (Exception ex)
            {
                throw new ArgumentOutOfRangeException("Can not send same NabaztagAction multiple times within same request, or no two actions with same parameters", ex);
            }
            XmlDocument responseXml = SendAction(parameters);

            return responseXml.DocumentElement.SelectSingleNode("message").InnerText;
        }

        #region helper functions
        /// <summary>
        /// Get dictionary with the default parameters.
        /// </summary>
        /// <returns></returns>
        protected Dictionary<String, String> GetParameterDictionary()
        {
            Dictionary<String, String> parameters = new Dictionary<String, String>();

            //Add parameters that are required to prevent people from adding them (easily)
            parameters.Add("sn", Serial);
            parameters.Add("token", Token);

            return parameters;
        }


        /// <summary>
        /// If the result is bad we throw an exception
        /// </summary>
        /// <param name="response"></param>
        /// <exception cref="NabaztagResponseException">Any response non-normal raises exception</exception>
        protected void raisePossibleResultException(XmlDocument response)
        {
            String comment;
            String message;
            if (response.DocumentElement.SelectSingleNode("comment") == null || response.DocumentElement.SelectSingleNode("message") == null)
                return;//different format, should be okay

            comment = response.DocumentElement.SelectSingleNode("comment").InnerText;
            message = response.DocumentElement.SelectSingleNode("message").InnerText;

            switch (message)
            {
                case "ABUSESENDING": throw new ToManyMessagesSentException(comment);
                case "NOTV2RABBIT": throw new NotANabaztagTagException(comment);
                case "NOCORRECTPARAMETERS": throw new ParametersMissingException(comment);
                case "WEBRADIONOTSENT": throw new WebRadioNotSentException(comment);
                case "EARPOSITIONNOTSENT": throw new EarPositionException(comment);
                case "CHORNOTSENT": throw new ChoreographyException(comment);
                case "TTSNOTSENT": throw new TextToSpeechException(comment);
                case "NABCASTNOTSENT": throw new NabCastNotSentException(comment);
                case "MESSAGENOTSENT": throw new MessageNotSentException(comment);
                case "NOGOODTOKENORSERIAL": throw new CredentialsException(comment);
            }
        }

        #endregion

        #region rabbit awake/sleeping status methods
        /// <summary>
        /// Check if the nabaztag is currently sleeping
        /// </summary>
        /// <returns></returns>
        protected Boolean GetIsSleeping()
        {
            Dictionary<String, String> parameters = GetParameterDictionary();
            parameters.Add("action", "7");
            XmlDocument response = SendAction(parameters);
            return response.SelectSingleNode("//rabbitSleep").InnerText.Equals("YES");
        }

        /// <summary>
        /// Toggle rabbit from sleeping to awake and vica versa
        /// </summary>
        /// <param name="toSleeping"></param>
        private void SetRabbitStatus(Boolean toSleeping)
        {
            Dictionary<String, String> parameters = GetParameterDictionary();
            if (toSleeping)
                parameters.Add("action", "13");//to sleep
            else
                parameters.Add("action", "14");//awake my friend!
            //this action can possible return an exception
            raisePossibleResultException(SendAction(parameters));
        }
        #endregion

        /// <summary>
        /// Fetch version information about current rabbit
        /// </summary>
        /// <returns></returns>
        protected NabaztagVersion GetVersion()
        {
            Dictionary<String, String> parameters = GetParameterDictionary();
            parameters.Add("action", "8");
            XmlDocument response = SendAction(parameters);
            return (NabaztagVersion)Enum.Parse(typeof(NabaztagVersion), response.SelectSingleNode("//rabbitVersion").InnerText.Substring(1));
        }


        /// <summary>
        /// Get a list of your Nabaztag's friends
        /// </summary>
        /// <returns></returns>
        public List<String> GetFriends()
        {
            Dictionary<String, String> parameters = GetParameterDictionary();
            parameters.Add("action", "2");
            XmlDocument response = SendAction(parameters);
            List<String> result = new List<String>();

            foreach (XmlAttribute attrib in response.SelectNodes("rsp//friend/@name"))
                result.Add(attrib.InnerText);

            return result;
        }

        /// <summary>
        /// Get a list of the messages in your violet inbox
        /// </summary>
        /// <returns></returns>
        public List<String> GetMessages()
        {
            Dictionary<String, String> parameters = GetParameterDictionary();
            parameters.Add("action", "3");
            XmlDocument response = SendAction(parameters);
            List<String> result = new List<String>();

            /*
             * response XML from violet service (currently)
                <?xml version="1.0" encoding="UTF-8"?>
                <rsp>
                <listreceivedmsg nb="1"/>
                <msg from="toto" title="my message" date="today 11:59" url="broad/001/948.mp3"/>
                </rsp>             
             */

            //this returns the xml elements in string list but i do not know where to use this implementation for :)
            foreach (XmlAttribute attrib in response.SelectNodes("rsp//msg"))
                result.Add(attrib.InnerXml);
            return result;
        }

        /// <summary>
        /// Get the timezone setting of the nabaztag
        /// </summary>
        /// <returns></returns>
        public String GetTimezone()
        {
            Dictionary<String, String> parameters = GetParameterDictionary();
            parameters.Add("action", "4");
            XmlDocument response = SendAction(parameters);
            return response.SelectSingleNode("//timezone").InnerText;
        }


        /// <summary>
        /// Get a list of all supported languagescodes for text to speech actions
        /// </summary>
        /// <returns></returns>
        public List<String> GetSupportedLanguages()
        {
            Dictionary<String, String> parameters = GetParameterDictionary();
            parameters.Add("action", "9");
            XmlDocument response = SendAction(parameters);
            List<String> result = new List<String>();

            //add distinct language codes to output list
            foreach (XmlAttribute attrib in response.SelectNodes("rsp/voice/@lang"))
                if (!result.Contains(attrib.InnerText))
                    result.Add(attrib.InnerText);
            //make it ordered
            result.Sort();
            return result;
        }

        /// <summary>
        /// Fetch a list of voices for the text to speech engine, based on the given language code
        /// </summary>
        /// <param name="LanguageCode">NL-nl</param>
        /// <returns></returns>
        public List<String> GetSupportedVoices(String LanguageCode)
        {
            Dictionary<String, String> parameters = GetParameterDictionary();
            parameters.Add("action", "9");
            XmlDocument response = SendAction(parameters);
            List<String> result = new List<String>();

            foreach (XmlAttribute attrib in response.SelectNodes(string.Format("rsp/voice[@lang='{0}']/@command", LanguageCode)))
                result.Add(attrib.InnerText);
            return result;
        }

        /// <summary>
        /// Get name of the rabbit
        /// </summary>
        protected String GetName()
        {
            Dictionary<String, String> parameters = GetParameterDictionary();
            parameters.Add("action", "10");
            XmlDocument response = SendAction(parameters);
            return response.SelectSingleNode("//rabbitName").InnerText;
        }
    }
}
